package birdsong.server;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import birdsong.comm.ServerComm;
import birdsong.comm.ServerSideMessage;

public class Server implements BirdsongServer {
    /**
     * LinkedList of all the connected users (Clients)
     */
    private LinkedList<Client> onlineClientsList = new LinkedList<Client>();
    private ServerComm serverComm;

    // STANDARD ERROR MESSAGES
    public static final String ERROR_CLIENT_OFFLINE = "ERROR - Client offline";
    public static final String ERROR_FOLLOWER_D_OFFLINE = "ERROR - Follower(d) offline";
    public static final String ERROR_PAYLOAD_NULL = "ERROR - Payload is null";
    public static final String ERROR_SERVERCOMM_NULL = "ERROR - serverComm is null";

    public static final String ERROR_MESSAGE_NULL = "ERROR - serverComm.getNextMessage() returns null";
    public static final String ERROR_DUPLICATED_NICKNAME = "";

    private ArrayList<String> errorLog = new ArrayList<String>();
    private int numberOfErrors = 0;

    @Override
    /**
     * Starts the server which receives and processes messages and keeps track of the clients
     * @author Diana, Ines, Rafael, Ricardo
     * @param serverComm the serverComm through which the server should communicate with clients.
     *
     */
    public void start(ServerComm serverComm) {
	numberOfErrors = 0;
	errorLog.clear();
	this.serverComm = serverComm;

	// COMMENT
	boolean stopped = false;
	if (serverComm != null) {
	    serverComm.start();
	    while (!stopped) {

		// Getting the message, if any
		ServerSideMessage message = this.serverComm.getNextMessage();
		if (message != null) {
		    // BIRDSONG_MESSAGE
		    if (message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE) {
			try {
			    processBirdSongMessage(message);

			} catch (IllegalArgumentException ex) {
			    if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
				System.out.println(ERROR_CLIENT_OFFLINE);
			    } else {
				if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
				    serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
				}
			    }
			    errorLog.add(ex.getMessage());
			    numberOfErrors++;
			}
		    }

		    // CLIENT_CONNECTED
		    else if (message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
			processClientConnectedMessage(message);
		    }

		    // CLIENT_DISCONNECTED
		    else if (message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED) {
			try {
			    processClientDisconnectedMessage(message);

			} catch (IllegalArgumentException ex) {
			    if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
				errorLog.add(ERROR_CLIENT_OFFLINE);
				numberOfErrors++;
			    }
			}

		    }

		    // FOLLOW
		    else if (message.getType() == ServerSideMessage.Type.FOLLOW) {
			try {
			    processFollowMessage(message);

			} catch (IllegalArgumentException ex) {
			    if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
				System.out.println(ERROR_CLIENT_OFFLINE);
			    } else {
				if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
				    serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
				} else {
				    if (ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)) {
					serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
				    }
				}
			    }
			    errorLog.add(ex.getMessage());
			    numberOfErrors++;
			}
		    }

		    // UNFOLLOW
		    else if (message.getType() == ServerSideMessage.Type.UNFOLLOW) {
			try {
			    processUnfollowMessage(message);

			} catch (IllegalArgumentException ex) {
			    if (ex.getMessage().equals(ERROR_CLIENT_OFFLINE)) {
				/*
				 * The Network communications service shouldn't
				 * allow to send a message without a sender. But
				 * the service shouldn't go down because of it
				 */
				System.out.println(ERROR_CLIENT_OFFLINE);
			    } else {
				if (ex.getMessage().equals(ERROR_PAYLOAD_NULL)) {
				    serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
				} else {
				    if (ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)) {
					serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
				    }
				}
			    }
			    errorLog.add(ex.getMessage());
			    numberOfErrors++;
			}
		    }
		} else {
		    errorLog.add(ERROR_MESSAGE_NULL);
		    numberOfErrors++;
		    stopped = true;
		}
	    }
	} else {
	    errorLog.add(ERROR_SERVERCOMM_NULL);
	    numberOfErrors++;
	    throw new IllegalArgumentException(ERROR_SERVERCOMM_NULL);
	}
    }

    /**
     * Updates the relationship between the asker and the followed, that is,
     * asker unfollows the given client
     * 
     * @param message
     *            The message to be processed (must be from UNFOLLOW type and
     *            must have a payload)
     * @throws IllegalArgumentException
     *             Throws an exception whenever: The message hasn't a payload;
     *             The client isn't online; The desired followed isn't online;
     */
    private void processUnfollowMessage(ServerSideMessage message) throws IllegalArgumentException {
	// Checking the preconditions
	assert message.getType() == ServerSideMessage.Type.UNFOLLOW;

	// The client is asking to unfollow the user on the payload
	String askerNickname = message.getClientNickname();
	String followedNickname = message.getPayload();
	// Checking if payload has text
	if (message.getPayload() != null) {
	    Client asker = getClientByNickname(askerNickname);
	    // Checking if the asker is online - Precondition
	    if (asker != null) {
		Client followed = getClientByNickname(followedNickname);
		// Checking if the choosen client to follow is online
		if (followed != null) {
		    asker.removeFollowed(followed);
		    followed.removeFollower(asker);

		    // Warn the followed client about what happened;

		    serverComm.sendNewUnfollow(followedNickname, askerNickname);
		} else {
		    throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
		}
	    } else {
		throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
	    }
	} else {
	    throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
	}
    }

    /**
     * Updates the relationship between the asker and the followed, that is,
     * asker unfollows the given client
     * 
     * @param message
     *            The message to be processed (must be from FOLLOW type and must
     *            have a payload)
     * @throws IllegalArgumentException
     *             Throws an exception whenever: The message hasn't a payload;
     *             The client isn't online; The desired followed isn't online;
     */
    private void processFollowMessage(ServerSideMessage message) throws IllegalArgumentException {
	// The client is asking to follow the user on the payload
	assert message.getType() == ServerSideMessage.Type.FOLLOW;
	if (message.getPayload() != null) {
	    String askerNickname = message.getClientNickname();
	    String followedNickname = message.getPayload();
	    Client asker = getClientByNickname(askerNickname);

	    // Checking if asker is online
	    if (asker != null) {
		Client followed = getClientByNickname(followedNickname);
		if (followed != null) {
		    asker.addFollowed(followed);
		    followed.addFollower(asker);

		    // Warn the followed about what happened;

		    serverComm.sendNewFollower(followedNickname, askerNickname);
		} else {
		    throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
		}
	    } else {
		throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
	    }
	} else {
	    throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
	}
    }

    /**
     * Handles the disconnection of a client (sends a message to all users)
     * 
     * @param message
     *            The message to be processed (must be from CLIENT_DISCONNECTED
     *            type)
     * @throws IllegalArgumentException
     *             Throws an exception whenever: The client isn't online;
     */
    private void processClientDisconnectedMessage(ServerSideMessage message) throws IllegalArgumentException {
	assert message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED;
	String nickname = message.getClientNickname();

	Client clientGoing = getClientByNickname(nickname);
	// Checking if client is online
	if (clientGoing != null) {
	    // Delete client from the online client list
	    onlineClientsList.remove(clientGoing);

	    // Warn the all the users about the disconnection
	    for (Client client : onlineClientsList) {
		String clientNickname = client.getNickname();

		client.removeFollowed(clientGoing);
		client.removeFollower(clientGoing);
		serverComm.sendClientDisconnected(clientNickname, nickname);
	    }

	} else {
	    throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
	}
    }

    /**
     * Handles the connection of a new client
     * 
     * @param message
     *            The message to be processed (must be from CLIENT_CONNECTED
     *            type)
     */
    private void processClientConnectedMessage(ServerSideMessage message) {
	assert message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED;

	String nickname = message.getClientNickname();

	boolean nicknameIsAvailable = checkNicknameAvailability(nickname);

	// Checking if the choosen nickname is available, that is, if it isn't
	// already being used
	if (nicknameIsAvailable) {
	    Client justArrived = new Client(nickname);

	    // 1) Warn every one that there is a new client
	    // 2) And send the list of online users to the new client

	    for (Client client : onlineClientsList) {
		serverComm.sendClientConnected(client.getNickname(), nickname);
		serverComm.sendClientConnected(nickname, client.getNickname());
	    }

	    // Add the client to the online clients list
	    onlineClientsList.add(justArrived);

	} else {
	    serverComm.sendError(nickname, ERROR_DUPLICATED_NICKNAME);
	    serverComm.disconnectClient(nickname);
	}
    }

    /**
     * Checks if the nickname is available, that is, if the nickname isn't
     * already being used
     * 
     * @param nickname
     * @return
     */
    private boolean checkNicknameAvailability(String nickname) {
	// If there isn't a client with the specified nickname, it's available
	return getClientByNickname(nickname) == null;
    }

    /**
     * Sends the received Birdsong message to all the followers
     * 
     * @param message
     *            The message to be processed (must be from BIRDSONG_MESSAGE
     *            type and must have a payload)
     * @throws IllegalArgumentException
     *             Throws an exception whenever: The message hasn't a payload;
     *             The client isn't online;
     */
    private void processBirdSongMessage(ServerSideMessage message) throws IllegalArgumentException {

	assert message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE;

	String nickname = message.getClientNickname();
	String birdsongMessage = message.getPayload();
	// Check if birdsongMessade has content
	if (birdsongMessage != null) {
	    // Get the client

	    Client client = getClientByNickname(nickname);

	    // Checking if client is online

	    if (client != null) {
		// Get the followersList

		LinkedList<Client> followersList = client.getFollowersList();

		// Send the message to all the followers

		for (Client follower : followersList) {
		    String followerNickname = follower.getNickname();
		    serverComm.sendBirdsongMessage(nickname, followerNickname, birdsongMessage);
		}
	    } else {
		throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
	    }
	} else {
	    throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
	}
    }

    /**
     * Searches and returns the online client with the given nickname
     * 
     * @param nickname
     * @return Null if the client isn't online. Returns the online client with
     *         the given nickname
     */

    private Client getClientByNickname(String nickname) {

	Client foundClient = null;
	Iterator<Client> iterator = onlineClientsList.iterator();
	boolean found = false;
	boolean ended = !iterator.hasNext();

	while (!found && !ended) {
	    Client client = iterator.next();
	    if (client.getNickname().equals(nickname)) {
		found = true;
		foundClient = client;
	    } else {
		if (!iterator.hasNext()) {
		    ended = true;
		}
	    }
	}

	return foundClient;
    }

    /**
     * Gives access to the Error Log
     * 
     * @return errorLog An array of Error description
     */
    public ArrayList<String> getErrorLog() {
	return errorLog;
    }

    /**
     * Returns the error count during the last execution
     * 
     * @return numberOfErrors the number of errors that occurred during the last
     *         execution
     */
    public int getNumberOfErrors() {
	return numberOfErrors;
    }

    // public static void main(String[] args){
    // ServerCommDummy serverComm = new ServerCommDummy();
    // Server server = new Server();
    // server.start(serverComm);
    // }
}
